// PART OF THE MACHINE SIMULATION. DO NOT CHANGE.

package nachos.machine;

import nachos.security.*;

import java.io.IOException;
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.net.SocketException;

/**
 * A full-duplex network link. Provides ordered, unreliable delivery of
 * limited-size packets to other machines on the network. Packets are guaranteed
 * to be uncorrupted as well.
 * 
 * <p>
 * Recall the general layering of network protocols:
 * <ul>
 * <li>Session/Transport
 * <li>Network
 * <li>Link
 * <li>Physical
 * </ul>
 * 
 * <p>
 * The physical layer provides a bit stream interface to the link layer. This
 * layer is very hardware-dependent.
 * 
 * <p>
 * The link layer uses the physical layer to provide a packet interface to the
 * network layer. The link layer generally provides unreliable delivery of
 * limited-size packets, but guarantees that packets will not arrive out of
 * order. Some links protect against packet corruption as well. The ethernet
 * protocol is an example of a link layer.
 * 
 * <p>
 * The network layer exists to connect multiple networks together into an
 * internet. The network layer provides globally unique addresses. Routers
 * (a.k.a. gateways) move packets across networks at this layer. The network
 * layer provides unordered, unreliable delivery of limited-size uncorrupted
 * packets to any machine on the same internet. The most commonly used network
 * layer protocol is IP (Internet Protocol), which is used to connect the
 * Internet.
 * 
 * <p>
 * The session/transport layer provides a byte-stream interface to the
 * application. This means that the transport layer must deliver uncorrupted
 * bytes to the application, in the same order they were sent. Byte-streams must
 * be connected and disconnected, and exist between ports, not machines.
 * 
 * <p>
 * This class provides a link layer abstraction. Since we do not allow different
 * Nachos networks to communicate with one another, there is no need for a
 * network layer in Nachos. This should simplify your design for the
 * session/transport layer, since you can assume packets never arrive out of
 * order.
 */
public class NetworkLink {
	/**
	 * Allocate a new network link.
	 * 
	 * <p>
	 * <tt>nachos.conf</tt> specifies the reliability of the network. The
	 * reliability, between 0 and 1, is the probability that any particular
	 * packet will not get dropped by the network.
	 * 
	 * @param privilege
	 *            encapsulates privileged access to the Nachos machine.
	 */
	public NetworkLink(Privilege privilege) {
		System.out.print(" network");

		this.privilege = privilege;

		try {
			localHost = InetAddress.getLocalHost();
		} catch (UnknownHostException e) {
			localHost = null;
		}

		Lib.assertTrue(localHost != null);

		reliability = Config.getDouble("NetworkLink.reliability");
		Lib.assertTrue(reliability > 0 && reliability <= 1.0);

		socket = null;

		for (linkAddress = 0; linkAddress < Packet.linkAddressLimit; linkAddress++) {
			try {
				socket = new DatagramSocket(portBase + linkAddress, localHost);
				break;
			} catch (SocketException e) {
			}
		}

		if (socket == null) {
			System.out.println("");
			System.out.println("Unable to acquire a link address!");
			Lib.assertNotReached();
		}

		System.out.print("(" + linkAddress + ")");

		receiveInterrupt = new Runnable() {
			public void run() {
				receiveInterrupt();
			}
		};

		sendInterrupt = new Runnable() {
			public void run() {
				sendInterrupt();
			}
		};

		scheduleReceiveInterrupt();

		Thread receiveThread = new Thread(new Runnable() {
			public void run() {
				receiveLoop();
			}
		});

		receiveThread.start();
	}

	/**
	 * Returns the address of this network link.
	 * 
	 * @return the address of this network link.
	 */
	public int getLinkAddress() {
		return linkAddress;
	}

	/**
	 * Set this link's receive and send interrupt handlers.
	 * 
	 * <p>
	 * The receive interrupt handler is called every time a packet arrives and
	 * can be read using <tt>receive()</tt>.
	 * 
	 * <p>
	 * The send interrupt handler is called every time a packet sent with
	 * <tt>send()</tt> is finished being sent. This means that another packet
	 * can be sent.
	 * 
	 * @param receiveInterruptHandler
	 *            the callback to call when a packet arrives.
	 * @param sendInterruptHandler
	 *            the callback to call when another packet can be sent.
	 */
	public void setInterruptHandlers(Runnable receiveInterruptHandler,
			Runnable sendInterruptHandler) {
		this.receiveInterruptHandler = receiveInterruptHandler;
		this.sendInterruptHandler = sendInterruptHandler;
	}

	private void scheduleReceiveInterrupt() {
		privilege.interrupt.schedule(Stats.NetworkTime, "network recv",
				receiveInterrupt);
	}

	private synchronized void receiveInterrupt() {
		Lib.assertTrue(incomingPacket == null);

		if (incomingBytes != null) {
			if (Machine.autoGrader().canReceivePacket(privilege)) {
				try {
					incomingPacket = new Packet(incomingBytes);

					privilege.stats.numPacketsReceived++;
				} catch (MalformedPacketException e) {
				}
			}

			incomingBytes = null;
			notify();

			if (incomingPacket == null)
				scheduleReceiveInterrupt();
			else if (receiveInterruptHandler != null)
				receiveInterruptHandler.run();
		} else {
			scheduleReceiveInterrupt();
		}
	}

	/**
	 * Return the next packet received.
	 * 
	 * @return the next packet received, or <tt>null</tt> if no packet is
	 *         available.
	 */
	public Packet receive() {
		Packet p = incomingPacket;

		if (incomingPacket != null) {
			incomingPacket = null;
			scheduleReceiveInterrupt();
		}

		return p;
	}

	private void receiveLoop() {
		while (true) {
			synchronized (this) {
				while (incomingBytes != null) {
					try {
						wait();
					} catch (InterruptedException e) {
					}
				}
			}

			byte[] packetBytes;

			try {
				byte[] buffer = new byte[Packet.maxPacketLength];

				DatagramPacket dp = new DatagramPacket(buffer, buffer.length);

				socket.receive(dp);

				packetBytes = new byte[dp.getLength()];

				System.arraycopy(buffer, 0, packetBytes, 0, packetBytes.length);
			} catch (IOException e) {
				return;
			}

			synchronized (this) {
				incomingBytes = packetBytes;
			}
		}
	}

	private void scheduleSendInterrupt() {
		privilege.interrupt.schedule(Stats.NetworkTime, "network send",
				sendInterrupt);
	}

	private void sendInterrupt() {
		Lib.assertTrue(outgoingPacket != null);

		// randomly drop packets, according to its reliability
		if (Machine.autoGrader().canSendPacket(privilege)
				&& Lib.random() <= reliability) {
			// ok, no drop
			privilege.doPrivileged(new Runnable() {
				public void run() {
					sendPacket();
				}
			});
		} else {
			outgoingPacket = null;
		}

		if (sendInterruptHandler != null)
			sendInterruptHandler.run();
	}

	private void sendPacket() {
		Packet p = outgoingPacket;
		outgoingPacket = null;

		try {
			socket.send(new DatagramPacket(p.packetBytes, p.packetBytes.length,
					localHost, portBase + p.dstLink));

			privilege.stats.numPacketsSent++;
		} catch (IOException e) {
		}
	}

	/**
	 * Send another packet. If a packet is already being sent, the result is not
	 * defined.
	 * 
	 * @param pkt
	 *            the packet to send.
	 */
	public void send(Packet pkt) {
		if (outgoingPacket == null)
			scheduleSendInterrupt();

		outgoingPacket = pkt;
	}

	private static final int hash;
	private static final int portBase;

	/**
	 * The address of the network to which are attached all network links in
	 * this JVM. This is a hash on the account name of the JVM running this
	 * Nachos instance. It is used to help prevent packets from other users from
	 * accidentally interfering with this network.
	 */
	public static final byte networkID;

	static {
		hash = System.getProperty("user.name").hashCode();
		portBase = 0x4E41 + Math.abs(hash % 0x4E41);
		networkID = (byte) (hash / 0x4E41);
	}

	private Privilege privilege;

	private Runnable receiveInterrupt;
	private Runnable sendInterrupt;

	private Runnable receiveInterruptHandler = null;
	private Runnable sendInterruptHandler = null;

	private InetAddress localHost;
	private DatagramSocket socket;

	private byte linkAddress;
	private double reliability;

	private byte[] incomingBytes = null;
	private Packet incomingPacket = null;
	private Packet outgoingPacket = null;

}
